# coding: utf-8
#
# Copyright 2014 The Oppia Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS-IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Provides services for HTML skins for the reader view."""

__author__ = 'Sean Lip'

import copy
import inspect

from extensions.skins import skin_classes


class Registry(object):
    """Registry of all skins."""

    # Dict mapping skin ids to their classes.
    _skins_dict = {}

    @classmethod
    def _refresh_registry(cls):
        cls._skins_dict.clear()

        # Add new skin classes to the registry.
        for name, clazz in inspect.getmembers(skin_classes, inspect.isclass):
            if name.endswith('_test') or name == 'BaseSkin':
                continue

            ancestor_names = [
                base_class.__name__ for base_class in inspect.getmro(clazz)]
            if 'BaseSkin' not in ancestor_names:
                continue

            cls._skins_dict[clazz.skin_id] = clazz

    @classmethod
    def get_all_skin_ids(cls):
        """Get a list of all skin ids."""
        if not cls._skins_dict:
            cls._refresh_registry()
        return cls._skins_dict.keys()

    @classmethod
    def get_all_skin_classes(cls):
        """Get a dict mapping skin ids to skin classes."""
        if not cls._skins_dict:
            cls._refresh_registry()
        return copy.deepcopy(cls._skins_dict)

    @classmethod
    def get_skin_templates(cls, skin_ids):
        """Returns the concatanated HTML for the given skins.

        Raises an error if any of the skins is not found.
        """
        cls._refresh_registry()
        return '\n'.join([
            cls._skins_dict[skin_id].get_html() for skin_id in skin_ids])

    @classmethod
    def get_skin_js_url(cls, skin_id):
        """Returns the URL to the directive JS code for a given skin.

        Refreshes once if the skin id is not found; subsequently, throws an
        error.
        """
        if skin_id not in cls._skins_dict:
            cls._refresh_registry()
        return cls._skins_dict[skin_id].get_js_url()

    @classmethod
    def get_skin_tag(cls, skin_id):
        """Returns an HTML tag corresponding to the given skin.

        Refreshes once if the skin id is not found; subsequently, throws an
        error.
        """
        if skin_id not in cls._skins_dict:
            cls._refresh_registry()
        return cls._skins_dict[skin_id].get_tag()

# GadgetPanel constants
PANEL_IN_PRODUCTION = 'panel-in-production'
STACKABLE_AXIS = 'stackable-axis'
FIXED_AXIS = 'fixed-axis'
POSITION = 'position'
HORIZONTAL = 'horizontal'
VERTICAL = 'vertical'
LEFT = 'left'
RIGHT = 'right'
BOTTOM = 'bottom'
GADGET = 'gadget'
WIDTH = 'width'
HEIGHT = 'height'

# Default parameters
PIXEL_BUFFER_BETWEEN_GADGETS = 80
DEFAULT_PANEL_CAPACITY = 1

DEFAULT_GADGET_PANELS = {
    LEFT: {WIDTH: 100,
            HEIGHT: 450,
            STACKABLE_AXIS: VERTICAL,
            POSITION: LEFT},
    RIGHT: {WIDTH: 160,
            HEIGHT: 450,
            STACKABLE_AXIS: VERTICAL,
            POSITION: RIGHT
              },
    BOTTOM: {WIDTH: 320,
             HEIGHT: 100,
             STACKABLE_AXIS: HORIZONTAL,
             POSITION: BOTTOM}
}

class GadgetPanel(object):
    """ An area of the reader view reserved for the placement of Gadgets."""

    def __init__(self, width, height, stackable_axis, position):
        """
        Args:
        - width, int: Width in pixels
        - height, int: Height in pixels
        - stackable_axis, str: Which axis is able to stretch in length (the
            other automatically becomes the fixed_axis.)
        - position, str: typically one of [LEFT, RIGHT, BOTTOM], skins
            that desire additional panels may specify additional positions
            as long as corresponding positions are added to the skin.

        """
        self.width = None
        self.height = None
        self.stackable_axis = None
        self.position = None
        self.gadgets = []
        self.capacity = DEFAULT_PANEL_CAPACITY

        if stackable_axis == HORIZONTAL:
            setattr(self, FIXED_AXIS, VERTICAL)
        else:
            setattr(self, FIXED_AXIS, HORIZONTAL)

    def add_gadget(self, gadget):
        """ Add a gadget to this panel.

        Args:
        - gadget: instance of a Gadget subclass

        Returns 1 or 0 for success or failure respectively.
        """

        if self._validate_addition_of_new_gadget(gadget):
            self.gadgets.append(gadget)
            return 1
        else:
            return 0

    def _validate_addition_of_new_gadget(self, gadget):
        """ Validates that panel is able to accept the new gadget. """

        if not self.has_capacity:
            return 0
        elif not self._validate_dimensions_fit_of_new_gadget(gadget):
            return 0
        else:
            return 1

    def _validate_dimensions_fit_of_new_gadget(self, gadget):
        """ Validates acceptable fit for various gadget scenarios. """

        gadget_width, gadget_height = gadget.get_width(), gadget.get_height()

        # Verify base dimensions for the new gadget fit.
        if gadget_width > self.width:
            return 0
        elif gadget_height > self.height:
            return 0

        # Check multiple gadget dimensions if necessary
        if len(self.gadgets) > 1:

            if self.stackable_axis == HORIZONTAL:
                stackable_size_limit = self.width
                total_gadgets_size = gadget_width
                for other_gadget in self.gadgets:
                    total_gadgets_size += other_gadget.get_width()
            else:
                stackable_size_limit = self.height
                total_gadgets_size = gadget_height
                for other_gadget in self.gadgets:
                    total_gadgets_size += other_gadget.get_height()

            necessary_buffer = PIXEL_BUFFER_BETWEEN_GADGETS * (
                len(self.gadgets) - 1)

            if total_gadgets_size + necessary_buffer > stackable_size_limit:
                return 0

        return 1

    @property
    def has_capacity(self):
        """ Returns True if capacity available. """
        return len(self.gadgets) < self.capacity

class GadgetPanelFactory(object):
    """ Facilitates creation of GadgetPanels.

    Initial implementation only supports 3 default sizes. """

    def __init__(self):
        """ Instantiate a GadgetPanel ready to accept settings. """
        setattr(self, PANEL_IN_PRODUCTION, GadgetPanel(0, 0, None, None))

    def get_default_panel_by_type(self, default_panel_type):
        """ Returns a GadgetPanel instance of the specified type.

        Args:
        - default_panel_type, string: one of [LEFT, RIGHT, BOTTOM]

        Note: This method bypasses the custom factory route, simply
        returning a GadgetPanel with known acceptable values. """

        params = DEFAULT_GADGET_PANELS[default_panel_type]
        gadget_panel = GadgetPanel(
            params[WIDTH],
            params[HEIGHT],
            params[STACKABLE_AXIS],
            params[POSITION]
            )

        return gadget_panel

    def initialize_new_panel(self):
        """ Re-initialize a clear GadgetPanel instance. """
        self.__init__()

    def validate_and_return_panel(self):
        """ Returns a valid GadgetPanel, or warns of missing attributes. """
        validation_errors = self._get_validation_errors()
        if validation_errors:
            print "GadgetPanel needs the following attributes: %s" % str(
                validation_errors)
        else:
            return self._panel_in_production

    def set_height(self, height_in_pixels):
        """ Set height in pixels of the panel in production. """
        setattr(self._panel_in_production, HEIGHT, height_in_pixels)

    def set_width(self, width_in_pixels):
        """ Set width in pixels of the panel in production. """
        setattr(self._panel_in_production, WIDTH, width_in_pixels)

    def set_stackable_axis(self, stackable_axis):
        """ Set stackable axis for panel in production. """
        if not stackable_axis in self._valid_stackable_axis_values:
            raise Exception('Unknown Axis: %s, expected one of: %s') % (
                str(stackable_axis),
                str(self._valid_stackable_axis_values)
                )

        if stackable_axis == HORIZONTAL:
            fixed_axis = VERTICAL
        else:
            fixed_axis = HORIZONTAL
        
        setattr(self._panel_in_production, STACKABLE_AXIS, stackable_axis)
        setattr(self._panel_in_production, FIXED_AXIS, fixed_axis)

    def set_position(self, position):
        """ Set position for the panel in production. """
        if not position in self._valid_panel_positions:
            raise Exception('Unknown Position: %s, expected one of: %s') % (
                str(position),
                str(self._valid_panel_positions)
                )

        setattr(self._panel_in_production, POSITION, position)

    def _get_validation_errors(self):
        """ Returns a list of invalid attributes, if any.

        Returns an empty list if no errors. """

        missing_attributes = []
        for required_attribute in self._required_attributes:
            if not getattr(self._panel_in_production, required_attribute):
                missing_attributes.append(required_attribute)
        return missing_attributes

    @property
    def _required_attributes(self):
        """ Attributes that must be set for a GadgetPanel to be ready. """
        return [WIDTH, HEIGHT, STACKABLE_AXIS, POSITION]

    @property
    def _valid_stackable_axis_values(self):
        """ Valid options for setting the stackable axis. """
        return [HORIZONTAL, VERTICAL]

    @property
    def _valid_panel_positions(self):
        """ Valid options for panel positions. """
        return [LEFT, RIGHT, BOTTOM]

    @property
    def _panel_in_production(self):
        """ Convenience property for the GadgetPanel in production. """
        return getattr(self, PANEL_IN_PRODUCTION)
